<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\utils;

/**
 * Utility class for HTTP-related operations.
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\utils
 * @since 1.0.0
 */
final class HttpHelper
{
	/**
	 * Known AJAX HTTP headers
	 * @var array
	 */
	private static $_AJAX_HEADERS = array(
		'HTTP_X_REQUESTED_WITH', // the expected ajax header
		'X-Requested-With', // in case of CGI header rewriting
		'x-requested-with'  // another possible header rewriting
	);
	
	/**
	 * Hidden constructor
	 */
	private function __construct()
	{
	}
	
	/**
	 * Create a HTTP response header or append a new value to it.
	 * 
	 * @param string $name The HTTP header
	 * @param string $value The value to append
	 * @param string $separator The value separator
	 */
	public static function appendResponseHeader($name, $value, $separator = ' ')
	{
		$header = self::getResponseHeader($name);
		
		if ($header === false)
		{
			$header = $name . ': ' . $value;
		}
		else
		{
			$header = $name . ': ' . $header . $separator . $value;
		}
		
		header($header);
	}
	
	/**
	 * Extract a map of quality factor (ie preference) indexed by accepted items. The
	 * map is sorted by ascending preference.
	 * 
	 * @param string $header The acceptance HTTP header
	 * @return array A quality map of items, sorted by ascending preference
	 */
	private static function extractAcceptedItems($header)
	{
		$acceptedItems = preg_split( '/\s*,\s*/', strtolower($_SERVER[$header]) );
		
		$items = array();
		$qualityFactor = 1;
		
		foreach ($acceptedItems as $acceptedItem)
		{
			$parts = preg_split( '/\s*;\s*/', trim($acceptedItem) );
			
			$qualityFactor = self::extractQualityFactor($qualityFactor, $parts);
			
			$item = implode(';', $parts);
			$items[$item] = (float) $qualityFactor;
		}
		
		asort($items);
		
		return $items;
	}
	
	/**
	 * Lookup a quality factor for an accepted item. Remove the quality factor part
	 * from the exploded item if any.
	 * 
	 * @param float $qualityFactor The previous quality factor
	 * @param array $parts The current exploded item
	 * @return float The current item quality factor
	 */
	private static function extractQualityFactor($qualityFactor, array &$parts)
	{
		$hasQualityFactor = false;
		$lastPartIndex = count($parts) - 1;
		
		if ($lastPartIndex > 0)
		{
			$lastPart = $parts[$lastPartIndex];
			$hasQualityFactor = strncmp($lastPart, 'q=', 2) == 0;
		}
		
		if ($hasQualityFactor)
		{
			$qualityFactor = substr($lastPart, 2);
			array_pop($parts);
		}
		
		return $qualityFactor;
	}
	
	/**
	 * Get the accepted items, sorted by descending preference, from a quality map of items.
	 * 
	 * @param $items A quality map of items, sorted by ascending preference
	 */
	private static function getAcceptedItems($items)
	{
		return array_keys( array_reverse($items, true) );
	}
	
	/**
	 * Get the content types accepted by the user agent, sorted by descending preference.
	 * 
	 * @return array The accepted content types
	 */
	public static function getAcceptedContentTypes()
	{
		$contentTypes = self::extractAcceptedItems('HTTP_ACCEPT');
		return self::getAcceptedItems($contentTypes);
	}
	
	/**
	 * Get the charsets accepted by the user agent, sorted by descending preference.
	 * 
	 * @return array The accepted charsets
	 */
	public static function getAcceptedCharsets()
	{
		$contentTypes = self::extractAcceptedItems('HTTP_ACCEPT_CHARSET');
		return self::getAcceptedItems($contentTypes);
	}
	
	/**
	 * Get the encodings accepted by the user agent, sorted by descending preference.
	 * 
	 * @return array The accepted encodings
	 */
	public static function getAcceptedEncodings()
	{
		$encodings = self::extractAcceptedItems('HTTP_ACCEPT_ENCODING');
		return self::getAcceptedItems($encodings);
	}
	
	/**
	 * Get the languages accepted by the user agent, sorted by descending preference.
	 * 
	 * @return array The accepted languages
	 */
	public static function getAcceptedLanguages()
	{
		$languages = self::extractAcceptedItems('HTTP_ACCEPT_LANGUAGE');
		self::putSimpleLanguages($languages);
		return self::getAcceptedItems($languages);
	}
	
	/**
	 * Add missing simple languages (without the country part). Each simple language is added
	 * with a lower quality factor than the lowest quality factor for the language. Sort the
	 * array  by ascending preference.
	 * 
	 * @param array $languages The array to fill
	 */
	private static function putSimpleLanguages(array &$languages)
	{
		foreach($languages as $language => $qualityFactor)
		{
			$parts = explode('-', $language);
			
			if ( !array_key_exists($parts[0], $languages) )
			{
				$qualityFactor -= 0.1;
				
				while ($qualityFactor > 0 && array_search($qualityFactor, $languages, true) !==  false)
				{
					$qualityFactor -= 0.01;
				}
				
				$languages[ $parts[0] ] = max(0, $qualityFactor);
			}
		}
		
		asort($languages);
	}
	
	/**
	 * Get the current content type of the response.
	 * 
	 * @return string The content type
	 */
	public static function getContentType()
	{
		return self::getResponseHeader('Content-Type');
	}
	
	/**
	 * Get the MIME part of the current content type of the response.
	 * 
	 * @return string The content type MIME
	 */
	public static function getContentTypeMime()
	{
		$contentType = explode( ';', self::getContentType() );
		return trim( $contentType[0] );
	}
	
	/**
	 * Get the request's method (such as get, post, put...)
	 * 
	 * @return string The lowercase request method
	 */
	public static function getRequestMethod()
	{
		if ( array_key_exists('REDIRECT_REQUEST_METHOD', $_SERVER) )
		{
			return strtolower( $_SERVER['REDIRECT_REQUEST_METHOD'] );
		}
		
		return strtolower( $_SERVER['REQUEST_METHOD'] );
	}
	
	/**
	 * Get the request's path
	 * 
	 * @return string The request path
	 */
	public static function getRequestPath()
	{
		$path = @parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
		
		if ($path == false)
		{
			return $_SERVER['REQUEST_URI'];
		}
		
		return $path;
	}
	
	/**
	 * Get a response header by its name.
	 * 
	 * @param $name The name of the response header
	 * @return mixed The response header or false
	 */
	public static function getResponseHeader($name)
	{
		$name .= ':';
		
		foreach (headers_list() as $header)
		{
			if (stripos($header, $name) === 0)
			{
				$value = explode(':', $header);
				return $value[1];
			}
		}
		
		return false;
	}
	
	/**
	 * Check if the response currtently has a content encoding.
	 * 
	 * @return bool true if there is a content encoding
	 */
	public static function hasContentEncoding()
	{
		return self::hasResponseHeader('Content-Encoding');
	}
	
	/**
	 * Check if the response currtently has a specific header.
	 * 
	 * @param string $name The name of the header
	 * @return bool true if there is the header
	 */
	public static function hasResponseHeader($name)
	{
		return self::getResponseHeader($name) !== false;
	}
	
	/**
	 * Check if the current request is an AJAX request.
	 * 
	 * @return bool true if it's an AJAX request
	 */
	public static function isAjaxRequest()
	{
		foreach (self::$_AJAX_HEADERS as $header)
		{
			if ( isset($_SERVER[$header]) )
			{
				return strtolower( $_SERVER[$header] ) === 'xmlhttprequest';
			}
		}
		
		return false;
	}
	
	/**
	 * Make the response uncacheable by the user agent.
	 */
	public static function makeUncacheable()
	{
		if ($_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0')
		{
			self::setResponseHeader('Pragma', 'no-cache');
		}
		else
		{
			self::setResponseHeader('Cache-Control', 'no-cache');
		}
	}
	
	/**
	 * Populate the $_GET superglobal variable with the current request URI.
	 * This function leverages URL rewriting by restoring coherent parameters.
	 */
	public static function prepareGetParameters()
	{
		$urlQuery = @parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
		
		if ($urlQuery !== false)
		{
			$_GET = array();
			parse_str($urlQuery, $_GET);
		}
	}
	
	/**
	 * Notifies the user agent that the current request varies if requested with AJAX.
	 */
	public static function setAjaxVary()
	{
		self::appendResponseHeader('Vary', self::$_AJAX_HEADERS[0], ',');
	}
	
	/**
	 * Set the request's method (such as get, post, put...) but doesn't check if the method
	 * actually exists. This allows to inject custom methods. The classic use case of this
	 * method is the need to restore a saved request when authentication is needed as an
	 * intermediate step.
	 * 
	 * @param string The new request method, a single word
	 * @throws \InvalidArgumentException If $config is not a single word
	 */
	public static function setRequestMethod($method)
	{
		if (preg_match('/^\w+$/', $method) == 0)
		{
			throw new \InvalidArgumentException('Invalid method : ' . $method);
		}
		
		if ( array_key_exists('REDIRECT_REQUEST_METHOD', $_SERVER) )
		{
			$_SERVER['REDIRECT_REQUEST_METHOD'] = strtoupper($method);
		}
		else
		{
			$_SERVER['REQUEST_METHOD'] = strtoupper($method);
		}
	}
	
	/**
	 * Set the response location header for a redirection.
	 * 
	 * @param $location The new URL to reach
	 */
	public static function setResponseLocation($location)
	{
		self::setResponseHeader('Location', $location);
	}
	
	/**
	 * Set the content type of the current response.
	 * 
	 * @param string $contentType The content type to set
	 */
	public static function setContentType($contentType)
	{
		self::setResponseHeader('Content-Type', $contentType);
	}
	
	/**
	 * Set a response header. Override an existing header if any.
	 * 
	 * @param string $name The name of the header
	 * @param string $value The value of the header
	 */
	public static function setResponseHeader($name, $value)
	{
		header($name . ': ' . $value);
	}
	
	/**
	 * Set the HTTP status of the response. It is recommended to use a standard
	 * HTTP header. A custom status might be overriden by the HTTP server.
	 * 
	 * @param int $status The HTTP status
	 */
	public static function setResponseStatus($status)
	{
		header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status);
	}
}

?>